iT邦幫忙

2023 iThome 鐵人賽

DAY 22
0

今天要來學習的 Reducer 的應用,主要內容為 Reducer 的操作步驟。

較複雜的事件處理器

隨著程式越來越複雜,我們可能會遇到下面這樣的情況

像是在這裡,需要處理三種事件:「追加項目」、「刪除項目」、「編輯」。

import { useState } from "react";
import AddTask from "./AddTask.js";
import TaskList from "./TaskList.js";

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([
      ...tasks,
      {
        id: nextId++,
        text: text,
        done: false,
      },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) {
          return task;
        } else {
          return t;
        }
      })
    );
  }

  function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.id !== taskId));
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: "Visit Kafka Museum", done: true },
  { id: 1, text: "Watch a puppet show", done: false },
  { id: 2, text: "Lennon Wall pic", done: false },
];

若想要統一管理這樣的 event handlers,可以使用 Reducer 來操作,一共用三個步驟。

Step 1:從設定狀態(state setting)轉為派發動作(dispatch action)

這邊要做的事情是改變 event handlers 的內容,先將 state 相關的邏輯移除(這個後續會再處理)

,留下 event handlers 更純粹要操作事情,也就是告訴 React 使用者正在操作什麼事情,例如「新增了一個任務」、「更改了一個任務」或「刪除了一個任務」。實際來看看程式碼:

  • Before:
function handleAddTask(text) {
  setTasks([
    ...tasks,
    {
      id: nextId++,
      text: text,
      done: false,
    },
  ]);
}
  • After:
function handleAddTask(text) {
  dispatch({
    type: "added",
    id: nextId++,
    text: text,
  });
}

我們使用了 dispatch 去做這件事,可以看見 dispatch 內部傳入了一個 object,這個 object 稱為「action object」。

「action object」的內部通常可以在type以 string 來說明發生什麼事,其他可以按需求傳入額外的資訊即可。像是這樣子:

dispatch({
  // 描述發生什麼事情
  type: "what_happened",
  // 其他額外資訊
});

Step 2 : 撰寫 reducer function

在 Reducer function 我們要使用剛剛的 action object 和先前拿掉的 state 的邏輯。

function yourReducer(state, action) {
  // return next state for React to set
}

Reducer function 會有二個參數,一個是 state 的邏輯,另一個是 action object。先讓我們使用 if/else statement 來寫寫看:

function tasksReducer(tasks, action) {
  if (action.type === "added") {
    return [
      ...tasks,
      {
        id: action.id,
        text: action.text,
        done: false,
      },
    ];
  } else if (action.type === "changed") {
    return tasks.map((t) => {
      if (t.id === action.task.id) {
        return action.task;
      } else {
        return t;
      }
    });
  } else if (action.type === "deleted") {
    return tasks.filter((t) => t.id !== action.id);
  } else {
    throw Error("Unknown action: " + action.type);
  }
}

像以上這樣透過判定 action object 的 type (也就是第一步的要操作什麼事情)來決定回傳的東西。要留意這邊回傳的會是「下一個 state」(next state)。另外,這邊雖然是使用 if/else statement 來操作,在通常的情況下多為使用 switch statement。這樣子就完成第二步的操作了。

Step 3:在元件中使用 reducer

最後要來把剛剛學的結合起來。

首先需要先 import useReducer

import { useReducer } from "react";

再來,將useState取代為useReducer

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

這邊需要留意的是和僅需要初始值的useState不同useReducer需要「reducer function」和「初始的 state(initial state)」二個 arguments。而它 return 一個「狀態值(stateful value)」和「dispatch function」。

回顧一下,狀態值(stateful value)是什麼?

狀態值是表示應用程式當前狀態的一個值。它代表了應用程式的數據或狀態的快照。而在目前的例子 return 的 dispatch 用於觸發列表的更新,並會在 event handlers 當中被呼叫。

將全部結合起來就會是以下的程式碼:

import { useReducer } from "react";
import AddTask from "./AddTask.js";
import TaskList from "./TaskList.js";

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: "added",
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: "changed",
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: "deleted",
      id: taskId,
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case "added": {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case "changed": {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case "deleted": {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error("Unknown action: " + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: "Visit Kafka Museum", done: true },
  { id: 1, text: "Watch a puppet show", done: false },
  { id: 2, text: "Lennon Wall pic", done: false },
];

useState vs useReducer

讓我們比較看看useState和useReducer:

方面 useState useReducer
程式碼大小 需要較少的程式碼 需要定義 reducer 函數和派發 actions
可讀性 當狀態更新簡單時容易閱讀 當狀態更新複雜時,更容易分離更新邏輯
調試 難以找到狀態設置錯誤的位置和原因 可以在 reducer 中添加 console log 以更清晰地追蹤狀態更新和原因
測試 需要在元件內測試 可以將 reducer 單獨測試

至於要選用 useState 還是 useReducer,這個可以依據專案的需求或是偏好而定。

參考資料

  • React 官方文件 - Extracting State Logic into a Reducer

上一篇
Day 21 - 保存和重置 state
下一篇
Day 23 - prop drilling 和 context
系列文
30 days of React 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言